﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
#if NET40
using System.Linq.Expressions;
#else
using Microsoft.Scripting.Ast;
#endif
using CoNatural.Binders;

namespace CoNatural.Data.Binders {
	/// <summary>
	/// To be used by router when binding Script commands to IDbCommand
	/// </summary>
	public class ScriptCommandBinderDelegateFactory : IBinderDelegateFactory {
		public IDbProvider DbProvider { get; private set; }

		public ScriptCommandBinderDelegateFactory(IDbProvider dbProvider) {
			DbProvider = dbProvider;
		}

		#region IBinderDelegateFactory Members

		public IBinder CreateBinder(object source, Type targetType) {
			// Source must be a Script (only supports input binding)
			var script = source as Script;
			if (script == null)
				throw new Exception("Source must be a Script");

			var sourceParam = Expression.Parameter(typeof(object), "source");
			var targetParam = Expression.Parameter(typeof(object), "target");
			var binderBlock = GetBinderBlock(script, sourceParam, targetParam);
			var mapperBlock = GetMapperBlock(script);
			return new BinderBase(
				Expression.Lambda<BinderDelegate>(binderBlock, sourceParam, targetParam).Compile(),
				Expression.Lambda<MapperDelegate>(mapperBlock, sourceParam).Compile()
			);
		}

		#endregion

		#region Expressions

		private BlockExpression GetBinderBlock(Script script, ParameterExpression sourceParam, ParameterExpression targetParam) {
			var end = Expression.Label(typeof(object), "end");
			var dbprov = Expression.Variable(typeof(IDbProvider), "dbp");
			var source = Expression.Variable(typeof(Script), "s");
			var target = Expression.Variable(typeof(IDbCommand), "t");

			return Expression.Block(
				new[] { dbprov, source, target },
				Expression.Assign(dbprov, Expression.Convert(targetParam, typeof(IDbProvider))),
				Expression.Assign(source, Expression.Convert(sourceParam, typeof(Script))),
				Expression.Assign(target, Expression.Call(dbprov, typeof(IDbProvider).GetMethod("CreateCommand"))),
				Expression.Block(GetBinderExpressions(script, dbprov, source, target, end))
			);
		}

		private IEnumerable<Expression> GetBinderExpressions(Script script, Expression dbprov, Expression source, Expression target, LabelTarget end) {
			if (script.Parameters != null) {
				foreach (var kvp in script.Parameters) {
					var p = DbProvider.CreateParameter(kvp.Key, kvp.Value.GetType(), null);
					// create parameter and set input value from mapped property
					var scriptparameters = Expression.Property(source, typeof(Script).GetProperty("Parameters"));
					var itemprop = typeof(IDictionary<string, object>).GetProperty("Item", new[] { typeof(string) });
					var paramvalue = Expression.Property(scriptparameters, itemprop, Expression.Constant(p.Name));
					var targetparam = Expression.Call(
						dbprov,
						typeof(IDbProvider).GetMethod("CreateDataParameter"),
						Expression.Constant(p.Name, typeof(string)),
						Expression.Constant(p.DbType, typeof(DbType)),
						Expression.Constant(p.Direction, typeof(ParameterDirection)),
						Expression.Constant(p.Size, typeof(int)),
						Expression.Constant(p.Precision, typeof(byte)),
						Expression.Constant(p.Scale, typeof(byte)),
						Expression.Constant(p.IsNullable, typeof(bool)),
						Expression.Constant(p.UdtName, typeof(string)),
						Expression.Convert(paramvalue, typeof(object))
					);
					var targetparams = Expression.Property(target, typeof(IDbCommand).GetProperty("Parameters"));
					yield return Expression.Call(targetparams, typeof(System.Collections.IList).GetMethod("Add"), targetparam);
				}
			}
			yield return Expression.Label(end, target);
		}

		private BlockExpression GetMapperBlock(Script script) {
			var maplist = Expression.Variable(typeof(List<IMapping>), "mappings");
			var end = Expression.Label(typeof(List<IMapping>), "end");

			return Expression.Block(
				new[] { maplist },
				Expression.Assign(maplist, Expression.New(typeof(List<IMapping>))),
				Expression.Block(GetMapperExpressions(script, maplist, end))
			);
		}

		private IEnumerable<Expression> GetMapperExpressions(Script script, Expression maplist, LabelTarget end) {
			if (script.Parameters != null) {
				foreach (var key in script.Parameters.Keys) {
					// create expression to construct new IMapping from mapping
					var constructor = typeof(MappingBase).GetConstructor(new[] { typeof(string), typeof(string), typeof(object) });
					var map = Expression.New(
						constructor,
						Expression.Constant(key),
						Expression.Constant(key),
						Expression.Constant(null)
					);
					yield return Expression.Call(maplist, typeof(List<IMapping>).GetMethod("Add"), map);
				}
			}
			yield return Expression.Label(end, maplist);
		}

		#endregion Expressions
	}
}
